<?php
/**
 * Created by PhpStorm.
 * User: ari
 * Date: 1/16/2015
 * Time: 12:07 AM
 */
namespace BC\Socket;


use BC\Config;
use BC\SiteIndex;
use BC\User\Session\UserSession;
use CPath\Render\Text\TextMimeType;
use CPath\Request\Session\SessionRequest;
use Wrench\Application\Application;
use Wrench\Client;
use Wrench\Connection;
use Wrench\Exception\Exception;
use Wrench\Payload\Payload;
use Wrench\Server;

require(dirname(__DIR__) . '/libs/wrench/lib/SplClassLoader.php');

$classLoader = new \SplClassLoader('Wrench', __DIR__ . '/../lib');
$classLoader->setIncludePath(dirname(__DIR__) . '/libs/wrench/lib/');
$classLoader->register();

if(!empty($argv) && realpath($argv[0]) ===  __FILE__) {
	require_once(dirname(__DIR__) . '/SiteIndex.php');
}

class WebSocket extends Application 
{
	const CLIENT_PORT = 7845;
	const CLIENT_HOST = '127.0.0.1';

	const EVENT_WATCH = 'watch';
	const EVENT_ERROR = 'error';
	const EVENT_MESSAGE = 'msg';
	const EVENT_DEBUG = 'debug';

//	const EVENT_LOG_UPDATE    = 'log-update';
	const WATCH_REGEX = '/.*\.log$/';

	/** @var Server */
	private $mServer = null;
	private $mSessionClients = array();
	private $mWatches = array();
//	private $_clients = array();

	function run() {

		$this->mServer = new Server('ws://' . self::CLIENT_HOST . ':' . self::CLIENT_PORT . '/', array(
			'allowed_origins'            => array(
				'localhost',
				'127.0.0.1',
			),
		));

		$this->mServer->registerApplication('listen', $this);
		$this->mServer->run();
	}

	static function createClient() {
		$Client = new Client('ws://' . self::CLIENT_HOST . ':' . self::CLIENT_PORT . '//', 'http://localhost');
		return $Client;
	}

	/**
	 * @param $eventName
	 * @param $msg
	 * @param Connection $connection
	 * @throws \Wrench\Exception\ConnectionException
	 * @throws \Wrench\Exception\HandshakeException
	 * @return bool
	 */
	private function send($eventName, $msg, $connection) {
		$connection->send($eventName . ' ' . $msg);
		echo $eventName . ' ' . $msg . "\n";
		return true;
	}

	/**
	 * @param Connection $client
	 * @return bool
	 */
	private function hasSessionID($client) {
		$headers = $client->getHeaders();
		if($cookies = $headers['Cookie']) {
			parse_str(str_replace('; ', '&', $cookies), $cookies);
			if (!empty($cookies[session_name()]))
				return $cookies[session_name()];
		}
		return false;
	}

	/**
	 * @param Connection $client
	 */
	public function onConnect($client)
	{
		echo "Client connected: " . $client->getIp() . "\n";
		if($sessionID = $this->hasSessionID($client)) {
			session_id($sessionID);
			@session_start();
			$SessionRequest = new SessionRequest();
			$sessionFP = UserSession::getFingerprintFromSession($SessionRequest);
			$sessionUserID = UserSession::getSessionIDFromSession($SessionRequest);
			session_write_close();
			$this->mSessionClients[$client->getId()] = array($sessionID, $sessionFP, $sessionUserID);
			echo "Session detected: " . $sessionUserID . "\n";
		}

//		$this->_clients[$id] = $client;
//		$this->_sendServerinfo($client);
//		$client->send(self::EVENT_MESSAGE, " Clients connected: " . $this->mWatches)
	}

	/**
	 * @param $filePath
	 * @param $offset
	 * @param Connection $connection
	 * @return int
	 */
	private function readFile($filePath, $offset, $connection) {
//		$this->send(self::EVENT_DEBUG, "Checking {$filePath} (from {$offset})", $connection);

		$offset = (int)$offset;
		$oldOffset = $offset;
		$handle = fopen($filePath, 'r');
		if(!$handle)
			throw new \InvalidArgumentException("File could not be read: " . $filePath);
		fseek($handle, $offset);
		while (($line = fgets($handle, 4096)) !== false) {
			$offset += strlen($line);
			$connection->send(rtrim($line));
			echo "OUT: ", $line;
		}
		fpassthru($handle);
		fclose($handle);

		$fsize = filesize($filePath);
		if($offset !== $fsize)
			$this->send(self::EVENT_ERROR, "Offset does not match file size ({$oldOffset}) ({$offset} !== {$fsize}): $filePath", $connection);

//		if($oldOffset !== $offset)
//			$this->send(self::EVENT_DEBUG, "Offset Updated {$oldOffset} to {$offset}: $filePath", $connection);

		return $offset;
	}

	/**
	 * Handle data received from a client
	 * @param Payload $payload A payload object, that supports __toString()
	 * @param Connection $connection
	 * @return bool
	 */
	public function onData($payload, $connection) {
		$lines = explode("\n", $payload);

		$isLocalClient = false;

		$sessionID = null;
		$sessionFP = null;
		$sessionUserID = null;
		if(isset($this->mSessionClients[$connection->getId()])) {
			list($sessionID, $sessionFP, $sessionUserID) = $this->mSessionClients[$connection->getId()];
		}

		for($i=0; $i<sizeof($lines); $i++) {
			$line = $lines[$i];
			$requestPath = $line;
			$args = array();
			if($p = strpos($requestPath, ' ')) {
				$args = explode(' ', $line);
				$requestPath = array_shift($args);
			}
			switch($requestPath) {
				case self::EVENT_WATCH:
					if(!$sessionFP)
						return $this->send(self::EVENT_ERROR, 'No session cookie detected', $connection);

					list($watchPath, $watchOffset) = $args + array(null, null);
					if(!preg_match(self::WATCH_REGEX, $watchPath))
						throw new \InvalidArgumentException("[{$sessionUserID}] Invalid watch: " . $watchPath);
					$watchPath = Config::getContentPath($watchPath);
					if(!file_exists($watchPath))
						return $this->send(self::EVENT_ERROR, "[{$sessionUserID}] Watch path does not exist: " . $watchPath, $connection);

					$watchOffset = $this->readFile($watchPath, $watchOffset, $connection);
					$this->mWatches[] = array($connection, $watchPath, $watchOffset);
					return $this->send(self::EVENT_DEBUG, "[{$sessionUserID}] Watching: " . $watchPath, $connection);

				case self::EVENT_MESSAGE:
					break;
//				case self::EVENT_LOG_UPDATE:
//					if(!$isLocalClient)
//						return $this->send(self::EVENT_ERROR, 'Only local client may update', $connection);
//
//					list($updatedPath, ) = $args + array(null, null);
//					$count = 0;
//					foreach($this->mWatches as $i => $watches) {
//						/** @var Connection $connection */
//						list($connection, $watchPath, $watchOffset) = $watches;
//						if($watchPath === $updatedPath) {
//							$path = Config::getContentPath($updatedPath);
//							$watchOffset = $this->readFile($path, $watchOffset, $connection);
//							$this->mWatches[$i][2] = $watchOffset;
//						}
//					}
//
//					return $this->send(self::EVENT_MESSAGE, "Write successful ({$count} client(s))", $connection);

				default:
					if(!$sessionID)
						return $this->send(self::EVENT_ERROR, "No Session ID found", $connection);

					$url = parse_url($requestPath);
					if(!$url)
						throw new \InvalidArgumentException("Invalid command: " . $requestPath);
					$requestPath = $url['path'];
					parse_str($url['query'], $args);
					echo "Starting session " . $sessionID . "\n";
					session_id($sessionID);
					@session_start();
					$SocketRequest = new SocketFormRequest($connection, SocketRequest::METHOD_SOCKET, $requestPath, $args, new TextMimeType());


					try {
						echo "Executing " . $line . "\n";
						SiteIndex::route($SocketRequest);
					} catch (\Exception $ex) {
						echo $ex;
					}

					echo "Closing session " . $sessionID . "\n";
					session_write_close();

					return $this->send(self::EVENT_DEBUG, "Command successful", $connection);
					break;
			}
		}
		return false;
	}

	public function checkFile($logPath) {
		$logPath = realpath($logPath);
		for($i=0; $i<sizeof($this->mWatches); $i++) {
			$watches = $this->mWatches[$i];

			/** @var Connection $connection */
			list($connection, $watchPath, $watchOffset) = $watches;
			if(!$connection->getSocket()->isConnected()) {
				echo "Connection is closed. Removing watch '{$watchPath}' "  . $connection->getId() . PHP_EOL;
				unset($this->mWatches[$i]);
				$this->mWatches = array_values($this->mWatches);
				$i--;
				continue;
			}
//			$path = Config::getContentPath($logPath);
			if(realpath($watchPath) === $logPath) {
				clearstatcache();
				$size = filesize($logPath);
				if($watchOffset >= $size)
					throw new \Exception("File offset gte last offset ({$watchOffset} >= {$size})");

				$watchOffset = $this->readFile($logPath, $watchOffset, $connection);
				if($watchOffset !== $size)
					throw new Exception("Offset size mismatch ({$watchOffset} !== {$size})");
				if($this->mWatches[$i][2] !== $watchOffset)
					$this->mWatches[$i][2] = $watchOffset;
			}
		}
	}
}

//
//$WebSocket = new WebSocket();
//$WebSocket->execute(Request::create());
if(!empty($argv) && realpath($argv[0]) ===  __FILE__) {
	$WebSocket = new WebSocket();
	$WebSocket->run();
}